package plugin

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
	"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
	"github.com/grafana/grafana-plugin-sdk-go/backend/log"
	"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	influxdbClientSDK "github.com/influxdata/influxdb1-client/v2"
	"github.com/robfig/cron/v3"
	"github.com/ssk9/obs/pkg/influxdb/influxql"
	influxdbModels "github.com/ssk9/obs/pkg/influxdb/models"
	"github.com/ssk9/obs/pkg/models"
	"github.com/ssk9/obs/pkg/obs"
	"go.opentelemetry.io/otel/trace"
)

// Make sure Datasource implements required interfaces. This is important to do
// since otherwise we will only get a not implemented error response from plugin in
// runtime. In this example datasource instance implements backend.QueryDataHandler,
// backend.CheckHealthHandler interfaces. Plugin should not implement all these
// interfaces - only those which are required for a particular task.
var (
	_ backend.QueryDataHandler      = (*Datasource)(nil)
	_ backend.CheckHealthHandler    = (*Datasource)(nil)
	_ instancemgmt.InstanceDisposer = (*Datasource)(nil)
)

// NewDatasource creates a new datasource instance.
func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
	config, err := models.LoadPluginSettings(settings)
	if err != nil {
		return nil, err
	}

	logger := backend.NewLoggerWith("logger", "🐲 OBS 🐯")

	var obsCli *obs.Client
	var dsInfo *influxdbModels.DatasourceInfo

	// OBS client
	obs.InitOBS(config.Secrets.ApiKey, config.Secrets.SecretKey, config.Endpoint)
	obsCli = obs.Cli()

	// influxdb client
	influxdbCli, err := influxdbClientSDK.NewHTTPClient(influxdbClientSDK.HTTPConfig{
		Addr:     settings.URL,
		Username: settings.User,
		Password: config.Secrets.Password,
	})
	if err != nil {
		return nil, err
	}

	// create influxdb database
	_ = createDatabase(influxdbCli)

	// TODO HTTP client
	opts, err := settings.HTTPClientOptions(ctx)
	if err != nil {
		return nil, err
	}

	client, err := httpclient.New(opts)
	if err != nil {
		return nil, err
	}

	// default for test
	httpMode := "POST"
	maxSeries := 1000
	version := "influxql"

	database := config.Database
	if database == "" {
		database = settings.Database
	}

	dsInfo = &influxdbModels.DatasourceInfo{
		HTTPClient:    client,
		URL:           settings.URL,
		DbName:        database,
		Version:       version,
		HTTPMode:      httpMode,
		TimeInterval:  "",
		DefaultBucket: "",
		Organization:  "",
		MaxSeries:     maxSeries,
		InsecureGrpc:  false,
		Token:         "",
		Timeout:       opts.Timeouts.Timeout,
	}

	ds := &Datasource{
		settings:    *config,
		obsCli:      obsCli,
		logger:      logger,
		dsInfo:      dsInfo,
		tracer:      tracing.DefaultTracer(),
		influxdbCli: influxdbCli,
	}

	// init cron
	c := cron.New()
	_, err = c.AddFunc("@every 2m", func() {
		err = ds.CollectOBSStorageInfo()
		if err != nil {
			logger.Error("❌ CollectOBSStorageInfo ❌", err)
		}
	})
	if err != nil {
		return nil, err
	}
	c.Start()

	return ds, nil
}

// Datasource is an example datasource which can respond to data queries, reports
// its health and has streaming skills.
type Datasource struct {
	settings    models.PluginSettings
	obsCli      *obs.Client
	httpCli     *http.Client
	logger      log.Logger
	dsInfo      *influxdbModels.DatasourceInfo
	tracer      trace.Tracer
	influxdbCli influxdbClientSDK.Client
}

// Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance
// created. As soon as datasource settings change detected by SDK old datasource instance will
// be disposed and a new one will be created using NewSampleDatasource factory function.
func (d *Datasource) Dispose() {
	// Clean up datasource instance resources.
}

// QueryData handles multiple queries and returns multiple responses.
// req contains the queries []DataQuery (where each query contains RefID as a unique identifier).
// The QueryDataResponse contains a map of RefID to the response for each query, and each response
// contains Frames ([]*Frame).
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
	// create response struct
	response := backend.NewQueryDataResponse()

	if d.settings.Channel == models.ChannelDirect {
		for _, q := range req.Queries {
			res := d.queryByDirect(ctx, req.PluginContext, q)
			response.Responses[q.RefID] = res
		}
		return response, nil
	}

	var err error
	response, err = influxql.Query(ctx, d.tracer, d.dsInfo, req)
	return response, err
}

type queryModel struct {
	Query        string // the influxql query statement
	QueryText    string // for test
	ResultFormat string // result format time series table logs

	OBSQueryType int8
}

func (d *Datasource) queryByDirect(_ context.Context, _ backend.PluginContext, query backend.DataQuery) backend.DataResponse {
	var response backend.DataResponse

	// Unmarshal the JSON into our queryModel.
	var qm queryModel

	err := json.Unmarshal(query.JSON, &qm)
	if err != nil {
		return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("json unmarshal: %v", err.Error()))
	}

	switch qm.OBSQueryType {
	// Table
	case models.OBSQueryTypeBuckets:
		if d.obsCli == nil {
			return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("obsCli nil error"))
		}
		buckets, err := d.obsCli.GetBuckets()
		if err != nil {
			return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("GetBuckets err: %v", err.Error()))
		}
		frame := data.NewFrame("response")
		bucketNames := make([]string, 0, len(buckets))
		createTimes := make([]string, 0, len(buckets))
		for _, bucket := range buckets {
			bucketNames = append(bucketNames, bucket.Name)
			createTimes = append(createTimes, bucket.CreationDate.Format("2006-01-02 15:04:05"))
		}
		frame.Fields = append(frame.Fields, data.NewField("BucketName", nil, bucketNames))
		frame.Fields = append(frame.Fields, data.NewField("createTime", nil, createTimes))
		response.Frames = append(response.Frames, frame)
	// TimeRange
	case models.OBSQueryTypeStorage:
		fallthrough
	// TimeRange
	case models.OBSQueryTypeCost:
		frame := data.NewFrame("response")

		frame.Fields = append(frame.Fields,
			data.NewField("time", nil, []time.Time{query.TimeRange.From, query.TimeRange.To}),
			data.NewField("values", nil, []int64{10, 20}),
		)
		response.Frames = append(response.Frames, frame)
	}

	return response
}

// CheckHealth handles health checks sent from Grafana to the plugin.
// The main use case for these health checks is the test button on the
// datasource configuration page which allows users to verify that
// a datasource is working as expected.
func (d *Datasource) CheckHealth(_ context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
	res := &backend.CheckHealthResult{}
	config, err := models.LoadPluginSettings(*req.PluginContext.DataSourceInstanceSettings)

	if err != nil {
		res.Status = backend.HealthStatusError
		res.Message = "Unable to load settings"
		return res, nil
	}

	if config.Secrets.ApiKey == "" {
		res.Status = backend.HealthStatusError
		res.Message = "API key is missing"
		return res, nil
	}

	return &backend.CheckHealthResult{
		Status:  backend.HealthStatusOk,
		Message: "Data source is working",
	}, nil
}
